package me.realirea;

import com.comphenix.protocol.utility.StreamSerializer;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import me.pleens.awals.annotation.Awals;
import me.pleens.awals.annotation.Awals.ApiVersion;
import me.pleens.awals.annotation.ColouredLogger;
import me.pleens.awals.annotation.Dependency;
import me.pleens.awals.annotation.lifecycle.Init;
import me.pleens.awals.api.lang.LanguageManager;
import me.realirea.api.TombstoneManager;
import me.realirea.api.event.TombstoneBreakEvent;
import me.realirea.api.event.TombstoneGenerateEvent;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.block.Block;
import org.bukkit.block.Sign;
import org.bukkit.block.sign.Side;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;

import javax.inject.Inject;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Stream;

@Awals(value = "Tombstone", authors = "Alirea", apiVersion = ApiVersion.v1_14)
@Dependency("ProtocolLib")
@ColouredLogger(prefix = "&7[&8Tombstone&7] &r")
public class Tombstone implements TombstoneManager {

    private static Tombstone instance;
    private final LanguageManager languageManager;
    private final NamespacedKey tombstoneKey;
    private final NamespacedKey deceasedKey;
    private final NamespacedKey deathTimeKey;
    private final NamespacedKey funeralKey;
    private static final Gson gson = new GsonBuilder().create();

    @Inject
    private Tombstone(Plugin plugin, LanguageManager languageManager) {
        instance = this;
        this.languageManager = languageManager;
        this.tombstoneKey = new NamespacedKey(plugin, "tombstone");
        this.deceasedKey = new NamespacedKey(plugin, "deceased");
        this.deathTimeKey = new NamespacedKey(plugin, "death-time");
        this.funeralKey = new NamespacedKey(plugin, "funeral");
    }

    @NotNull
    public static TombstoneManager getTombstoneManager() {
        if (instance == null) {
            throw new IllegalStateException();
        }
        return instance;
    }

    private boolean hasSide;
    private boolean hasUpdateBooleanBoolean;
    private Method getBlockStateMethod;

    @Init
    private void compatibleSupport() {
        String nms = Bukkit.getServer().getClass().getName().split("\\.")[3];
        String craftBlockStatesClassName = "org.bukkit.craftbukkit." + nms + ".block.CraftBlockStates";
        try {
            Class<?> craftBlockStatesClass = Class.forName(craftBlockStatesClassName, false, Bukkit.class.getClassLoader());
            getBlockStateMethod = craftBlockStatesClass.getDeclaredMethod("getBlockState", Block.class, boolean.class);
            getBlockStateMethod.setAccessible(true);
        } catch (ReflectiveOperationException e) {
            // ignore
        }
        try {
            Class.forName("org.bukkit.block.sign.Side");
            hasSide = true;
        } catch (ReflectiveOperationException e) {
            // ignore
        }
        try {
            Sign.class.getMethod("update", boolean.class, boolean.class);
            hasUpdateBooleanBoolean = true;
        } catch (ReflectiveOperationException e) {
            // ignore
        }
    }

    @Override
    @SuppressWarnings("deprecation")
    public void generateTombstone(@NotNull Player player, @NotNull Block block, @NotNull List<ItemStack> stacks) {
        TombstoneGenerateEvent tombstoneGenerateEvent = new TombstoneGenerateEvent(block, player, stacks);
        Bukkit.getPluginManager().callEvent(tombstoneGenerateEvent);
        if (tombstoneGenerateEvent.isCancelled()) {
            return;
        }
        try {
            // 设置方块为木牌 并添加一些文本
            block.setType(Material.OAK_SIGN);
            Sign sign = (Sign) (getBlockStateMethod == null ? block.getState() : getBlockStateMethod.invoke(null, block, false));
            if (hasSide) {
                Stream.of(Side.values()).map(sign::getSide).forEach(side -> {
                    side.setLine(0, ChatColor.GRAY + "[墓碑]");
                    side.setLine(2, ChatColor.GRAY + player.getName());
                });
            } else {
                sign.setLine(0, ChatColor.GRAY + "[墓碑]");
                sign.setLine(2, ChatColor.GRAY + player.getName());
            }
            if (hasUpdateBooleanBoolean) {
                sign.update(true, true);
            } else {
                sign.update();
            }
            // 使用 PersistentData 接口 将相关数据 存储到方块内
            PersistentDataContainer dataContainers = sign.getPersistentDataContainer();
            PersistentDataContainer dataContainer = dataContainers.getAdapterContext().newPersistentDataContainer();
            dataContainer.set(deceasedKey, PersistentDataType.STRING, tombstoneGenerateEvent.getPlayer().getUniqueId().toString());
            dataContainer.set(deathTimeKey, PersistentDataType.LONG, System.currentTimeMillis());
            dataContainer.set(funeralKey, PersistentDataType.STRING, serializeItemStacks(tombstoneGenerateEvent.getFuneral()));
            dataContainers.set(tombstoneKey, PersistentDataType.TAG_CONTAINER, dataContainer);
            // 发送提示信息
            Location location = block.getLocation();
            Map<String, Object> map = new HashMap<>(4);
            map.put("world", block.getWorld());
            map.put("x", location.getBlockX());
            map.put("y", location.getBlockY());
            map.put("z", location.getBlockZ());
            languageManager.sendMessage("tombstone.generate", player, map);
        } catch (ReflectiveOperationException e) {
            throw new IllegalStateException(e);
        }
    }

    @Override
    public boolean isTombstone(@NotNull Sign sign) {
        return sign.getPersistentDataContainer().has(tombstoneKey, PersistentDataType.TAG_CONTAINER);
    }

    @Override
    public TombstoneBreakEvent callTombstoneBreakEvent(@NotNull Player player, @NotNull Sign sign) {
        if (!isTombstone(sign)) {
            throw new IllegalArgumentException();
        }
        PersistentDataContainer dataContainer = sign.getPersistentDataContainer().get(tombstoneKey, PersistentDataType.TAG_CONTAINER);
        assert dataContainer != null;
        UUID deceased = UUID.fromString(Objects.requireNonNull(dataContainer.get(deceasedKey, PersistentDataType.STRING)));
        long deathTime = Objects.requireNonNull(dataContainer.get(deathTimeKey, PersistentDataType.LONG));
        List<ItemStack> funeral = deserializeItemStack(Objects.requireNonNull(dataContainer.get(funeralKey, PersistentDataType.STRING)));
        TombstoneBreakEvent tombstoneBreakEvent = new TombstoneBreakEvent(sign.getBlock(), player, deceased, deathTime, funeral);
        Bukkit.getPluginManager().callEvent(tombstoneBreakEvent);
        return tombstoneBreakEvent;
    }

    /**
     * 序列化物品
     *
     * @param stacks 物品
     * @return 序列化字符串
     */
    @NotNull
    private String serializeItemStacks(@NotNull List<ItemStack> stacks) {
        JsonArray jsonElements = new JsonArray();
        try {
            for (ItemStack stack : stacks) {
                jsonElements.add(StreamSerializer.getDefault().serializeItemStack(stack));
            }
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
        return gson.toJson(jsonElements);
    }

    /**
     * 反序列化物品
     *
     * @param input 序列化文本
     * @return 物品
     */
    @NotNull
    private List<ItemStack> deserializeItemStack(@NotNull String input) {
        List<ItemStack> items = new ArrayList<>();
        try {
            for (JsonElement element : gson.fromJson(input, JsonArray.class)) {
                items.add(StreamSerializer.getDefault().deserializeItemStack(element.getAsString()));
            }
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
        return items;
    }
}
